

namespace Stee.CAP8.MessageBus.Kafka.Tool.Menu
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Threading.Tasks;
    using NLog;
    using Stee.CAP8.MessageBus.Interfaces.Interfaces;
    using Stee.CAP8.MessageBus.Kafka.Admin;
    using Stee.CAP8.MessageBus.Kafka.Configurations;
    using Stee.CAP8.MessageBus.Kafka.Constants;
    using Stee.CAP8.MessageBus.Kafka.Utility;

    internal class AdminMenu : IDisposable
    {
        private readonly static Logger Logger = LogManager.GetCurrentClassLogger();


        #region Fields

        /// <summary>
        /// Path to topic configuration JSON file.
        /// JSON file must contain 2 sections: DeadLetterConfigs & Topics
        /// </summary>
        private string topicFilePath;

        /// <summary>
        /// Topic configuration for dead-letter queue topics
        /// </summary>
        private IDictionary<string, string> dlqTopicConfigurations;

        /// <summary>
        /// List of topic configuration mappings, each mapping a topic
        /// </summary>
        private IList<IDictionary<string, string>> topicConfigurations;

        /// <summary>
        /// Admin client
        /// </summary>
        private IAdmin adminClient;

        #endregion Fields


        #region Constructor

        /// <summary>
        /// Initializes a new instance of the AdminMenu class
        /// </summary>
        public AdminMenu() { }

        /// <summary>
        /// Destructor
        /// </summary>
        ~AdminMenu()
        {
            this.Dispose(false);
        }

        #endregion Constructor


        #region Public Methods

        /// <summary>
        /// Reads in bootstrap server and initialize admin client
        /// </summary>
        /// <returns>Return TRUE if initialized else FALSE</returns>
        internal bool InitializeAdminClient()
        {
            try
            {
                Logger.Info("Initialize admin client...");

                Console.Write("Enter bootstrap server of message broker: ");
                var bootstrapServer = Console.ReadLine().Trim();

                if (string.IsNullOrEmpty(bootstrapServer))
                {
                    var consoleMsg = "File path cannot be NULL or empty.";
                    var logMsg = $"{consoleMsg} File path: {bootstrapServer}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);

                    return false;
                }

                Console.Write("Initialize admin client...");
                var adminConfigs = new Dictionary<string, string>
                {
                    { "BootstrapServers", bootstrapServer },
                };

                this.adminClient = new AdminClientImpl();
                this.adminClient.Initialize(adminConfigs, this.OnAdminClientError);

                Logger.Info($"Admin client initialize state: {this.adminClient.IsInitialized}");
                Console.WriteLine(this.adminClient.IsInitialized ? "OK" : "Not OK");
                Console.WriteLine(string.Empty);

                return this.adminClient.IsInitialized;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Reads in JSON config file path and creates new topics
        /// </summary>
        /// <returns>Return TRUE if configuration is read else FALSE</returns>
        internal bool GetJSONTopicConfigurations()
        {
            try
            {
                Logger.Info("Get topic configurations...");

                //// Get file path
                Console.Write("Enter the path to JSON topic configuration: ");
                this.topicFilePath = Console.ReadLine().Trim();
                var isValid = true;

                if (string.IsNullOrEmpty(this.topicFilePath))
                {
                    var consoleMsg = $"{Environment.NewLine}File path cannot be NULL or empty.";
                    var logMsg = $"{consoleMsg} File path: {this.topicFilePath}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);

                    isValid = false;
                }
                else if (!File.Exists(this.topicFilePath))
                {
                    var consoleMsg = $"{Environment.NewLine}File does not exist.";
                    var logMsg = $"{consoleMsg} File path: {this.topicFilePath}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);

                    isValid = false;
                }

                //// Get section
                PrintMessage("Reading topics configurations...", "Read DLQ & topic configurations...", Logger.Info, false);

                if (!ConfigurationHelper.IsJsonConfigSectionExist(this.topicFilePath, ConfigurationConstants.TopicDLQConfigSection))
                {
                    var consoleMsg =
                        $"{Environment.NewLine}Dead-letter configuration, \"{ConfigurationConstants.TopicDLQConfigSection}\", cannot be NULL or empty.";
                    var logMsg = $"{consoleMsg} File path: {this.topicFilePath}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);

                    isValid = false;
                }
                else if (!ConfigurationHelper.IsJsonConfigSectionExist(this.topicFilePath, ConfigurationConstants.TopicConfigSection))
                {
                    var consoleMsg =
                        $"{Environment.NewLine}Topic configurations, \"{ConfigurationConstants.TopicConfigSection}\", cannot be NULL or empty.";
                    var logMsg = $"{consoleMsg} File path: {this.topicFilePath}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);

                    isValid = false;
                }

                if (isValid)
                {
                    this.dlqTopicConfigurations = ConfigurationHelper.ReadTopicConfiguration(
                        this.topicFilePath, ConfigurationConstants.TopicDLQConfigSection);
                    this.topicConfigurations = ConfigurationHelper.ReadMultiTopicConfiguration(
                        this.topicFilePath, ConfigurationConstants.TopicConfigSection);
                }

                Logger.Info($"Topic configurations retrieve state: {this.topicConfigurations?.Any() == true}");
                Console.WriteLine(isValid ? "OK" : "Not OK");
                Console.WriteLine(string.Empty);

                return isValid;
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Create new topics & dead-letter queue topics
        /// </summary>
        /// <returns>Return TRUE if created successfully else FALSE</returns>
        internal async Task<bool> CreateTopics()
        {
            try
            {
                PrintMessage("Creating broker topics...", "Start creating topics...", Logger.Info, false);

                if (this.topicConfigurations?.Any() != true)
                {
                    //// No topics configured/found
                    var consoleMsg = "No topic configurations found.";
                    var logMsg = consoleMsg;
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);
                }
                else if (!this.adminClient.IsInitialized)
                {
                    //// Admin client is not initialized
                    var consoleMsg = "Admin client is not initialized, unable to create topic(s).";
                    var logMsg = $"{consoleMsg} Admin init state: {this.adminClient.IsInitialized}";
                    PrintMessage(consoleMsg, logMsg, Logger.Warn, true);
                }
                else
                {
                    foreach (var item in this.topicConfigurations)
                    {
                        var topicName = item[TopicConfiguration.TopicConfigName];
                        var partitionCount = item[TopicConfiguration.TopicConfigNumOfPartitions];
                        var repFactor = item[TopicConfiguration.TopicConfigReplicationFactor];
                        var otherConfigs = ProcessOtherConfigs(item);

                        await this.adminClient.CreateNewTopicAsync(
                            topicName, int.Parse(partitionCount), short.Parse(repFactor), otherConfigs, this.dlqTopicConfigurations)
                            .ConfigureAwait(false);
                    }
                }

                Console.WriteLine("OK");

                return true;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Not OK");
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Dispose
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        #endregion Public Methods


        #region Private Methods

        /// <summary>
        /// Admin client broker handler
        /// </summary>
        /// <param name="errorCode">Broker error code</param>
        /// <param name="errorReason">Error reason in clear text</param>
        /// <param name="isFatal">Flag indicating fatal error</param>
        private void OnAdminClientError(string errorCode, string errorReason, bool isFatal)
        {
            if (string.IsNullOrEmpty(errorCode))
            {
                Logger.Warn($"Unexpected admin client error, no error code returned. Is fatal: {isFatal}");
            }
            else
            {
                Logger.Error($"Admin client error: {errorCode}, Is fatal: {isFatal}");
            }
        }

        /// <summary>
        /// Dispose
        /// </summary>
        /// <param name="disposing">If disposing equals true, the method has been called directly
        /// or indirectly by a user's code. Managed and unmanaged resources can be disposed.
        /// If disposing equals false, the method has been called by the runtime from inside the
        /// finalizer and you should not reference other objects. Only unmanaged resources can be disposed.</param>
        protected virtual void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    this.adminClient?.Dispose();
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        #endregion Private Methods


        #region Static Methods

        /// <summary>
        /// Read all other configurations as key collection mapping
        /// </summary>
        /// <param name="configs">Raw topic configuration</param>
        /// <returns>Return collection mapping not including mandatory configs</returns>
        private static IDictionary<string, string> ProcessOtherConfigs(IDictionary<string, string> configs)
        {
            try
            {
                if (configs?.Any() == true)
                {
                    var otherConfigs = new Dictionary<string, string>();

                    foreach (var item in configs)
                    {
                        if (!(ValidateHelper.StringEquals(item.Key, TopicConfiguration.TopicConfigName) ||
                            ValidateHelper.StringEquals(item.Key, TopicConfiguration.TopicConfigNumOfPartitions) ||
                            ValidateHelper.StringEquals(item.Key, TopicConfiguration.TopicConfigReplicationFactor)))
                        {
                            otherConfigs.Add(item.Key, item.Value);
                        }
                    }

                    return otherConfigs;
                }
                else
                {
                    return new Dictionary<string, string>();
                }
            }
            catch (Exception ex)
            {
                Logger.Error(ex, ex.Message);
                throw;
            }
        }

        /// <summary>
        /// Print message to console and create new log entry
        /// </summary>
        /// <param name="consoleMsg">Console message</param>
        /// <param name="logMsg">Log message</param>
        /// <param name="logAction">Log action(warn, error, info, ETC...)</param>
        /// <param name="isNewLine">Falg indicating to start on new console line</param>
        private static void PrintMessage(string consoleMsg, string logMsg, Action<string> logAction, bool isNewLine)
        {
            if (string.IsNullOrEmpty(consoleMsg))
            {
                throw new ArgumentNullException(nameof(consoleMsg));
            }
            else if (logMsg == null)
            {
                throw new ArgumentNullException(nameof(logMsg));
            }
            else if (logAction == null)
            {
                throw new ArgumentNullException(nameof(logAction));
            }

            if (isNewLine)
            {
                Console.WriteLine(consoleMsg);
            }
            else
            {
                Console.Write(consoleMsg);
            }

            logAction(logMsg);
        }

        #endregion Static Methods
    }
}
